#!/usr/bin/python
# -*- coding: utf-8 -*-

# Simplified BSD License (BSD)
# 
# Copyright (c) 2013, dr.kohlone@gmail.com
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
# 
# * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
# 
# * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

__version__ = "1.10"
__project_url__ = "https://omen.codeplex.com"


from __future__ import unicode_literals
import os
import shutil
import json
import re
import sys
import threading
import multiprocessing
import logging
import time

#----------------------------------------------------------------------------------------------------------#
#--------------------------------- classical patterns -----------------------------------------------------#
#----------------------------------------------------------------------------------------------------------#

# Generic Decorator class
class Decorator(object):
    def __init__(self, obj):
        self.obj = obj
        
    def __getattr__(self, attr):
        return getattr(self.obj, attr)

# singleton declaration
# Example usage:
#@singleton
#class MySingletonClass(object):
#    def __init__(self):
#        pass
#    def my_method(self):
#        pass
#
# MySingletonClass is now a singleton !
# ------------------------------------- 
def singleton(cls):
    instances = {}
    def get_instance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return get_instance


########################################################################
## code source from:
#            http://wiki.python.org/moin/PythonDecoratorLibrary
#
########################################################################

# method synchronization
# Example usage:
#from threading import Lock
#my_lock = Lock()
#@synchronized(my_lock)
#def critical1(*args):
#    # Interesting stuff goes here.
#    pass
#
#@synchronized(my_lock)
#def critical2(*args):
#    # Other interesting stuff goes here.
#    pass

def synchronized(lock_instance):
    def wrap(a_function):
        def new_function(*args, **kw):
            lock_instance.acquire()
            try:
                return a_function(*args, **kw)
            finally:
                lock_instance.release()
        return new_function
    return wrap


#----------------------------------------------------------------------------------------------------------#
#--------------------------------- module and factory tools------------------------------------------------#
#----------------------------------------------------------------------------------------------------------#

## Module tools class
class ModuleTools(object):
    ## add folder to path
    # @param folder_path path of the folder
    @staticmethod
    def add_module_folder(folder_path):
        path_to_import = sys.argv[0]
        path_to_import = os.path.dirname(path_to_import)
        path_to_import = path_to_import + '/' + folder_path
        sys.path.append(path_to_import)
    
    ## reload a module
    # @param module_name name of the module to reload
    @staticmethod
    def reload_module(module_name):
        modul = __import__(str(module_name))
        modul = reload(modul)

## Generic factory
class ClassFactory(object):
    ## get class from a module by class name
    # @param module_name name of the module containing class
    # @param class_name name of the class to load  
    @staticmethod
    def get_class(module_name, class_name):        
        modul = __import__(str(module_name))
        modul = reload(modul)
        clazz = modul.__dict__[class_name]
        return clazz
    
    @staticmethod
    def get_object_by_name(module_name, object_name, refresh=True):
        modul = __import__(str(module_name))
        if refresh:
            modul = reload(modul)
        obj = modul.__dict__[object_name]
        return obj
    
    @staticmethod
    def get_object_by_type(module_name, klass, refresh=True):
        result = []
        try:
            modul = __import__(str(module_name))
            if refresh:
                modul = reload(modul)
            for cl in dir(modul):
                obj = ClassFactory.get_object_by_name(module_name, cl, refresh=False)
                if type(obj) is type:
                    if issubclass(obj, klass):
                        result.append(obj)
        except StandardError, e:
            print str(e)
                    
        return result
        
#----------------------------------------------------------------------------------------------------------#
#--------------------------------- process and modules management------------------------------------------#
#----------------------------------------------------------------------------------------------------------#

class DaemonType(object):
    PROCESS = 'process'
    THREAD = 'thread'
        
# Daemon Factory
class DaemonFactory(object):
    @staticmethod
    def get_daemon(name, function, arguments, daemon_type=DaemonType.THREAD):
        if daemon_type in [DaemonType.PROCESS]:
            p = multiprocessing.Process(target=function, name=name, args=arguments)
            p.daemon = False
            return p
        else:
            t = threading.Thread(target=function, name=name, args=arguments)
            t.daemon = True
            return t



#----------------------------------------------------------------------------------------------------------#
#--------------------------------- file tools -------------------------------------------------------------#
#----------------------------------------------------------------------------------------------------------#
class FileTools(object):
    ## check if a file is present
    # @param file_path the full path of file to check
    # @return true if the specified file is present
    @staticmethod
    def is_file_present(file_path):
        return os.path.exists(file_path)
    
    ## read text from file
    # @param file_path full path of file to read
    # @return a str type object 
    @staticmethod
    def read_text(file_path):
        with open(file_path, 'r') as f:
            result = f.read()
        return result
    
    ## read text lines from file
    # @param file_path full path of file to read
    # @return a list type object containing file lines
    @staticmethod
    def read_lines(file_path):
        with open(file_path, 'r') as f:
            result = f.read_lines()
        return result
    
    ## read binary from file
    # @param file_path full path of file to read
    # @return the content
    @staticmethod
    def read_binary(file_path):
        with open(file_path, 'rb') as f:
            result = f.read()
        return result
    
    ## save text to file
    # @param text the text to save
    # @param file_path the full path of the output file
    # @return none
    @staticmethod
    def save_text(text, file_path, encoding='UTF-8'):
        #TODO: check encoding API
        with open(file_path, 'w') as f:
            f.write(text)
            
    ## save binary to file
    # @param data data to save
    # @param file_path the full path of the output file
    # @return none
    @staticmethod
    def save_binary(data, file_path):
        with open(file_path, 'wb') as f:
            f.write(data)
    
    ## delete a file
    # @param file_path full path of the file to delete
    # @param raise_except raise no exception if set to false. Default is true
    # @return none
    @staticmethod
    def delete_file(file_path, raise_exception=True):
        if not raise_exception:
            try:
                os.remove(file_path)
            except: # pylint: disable-msg=W0702
                return
        else:
            os.remove(file_path)
        
    @staticmethod
    def copy_folder(input_folder, output_folder):        
        shutil.copytree(input_folder, output_folder)
        
        
#----------------------------------------------------------------------------------------------------------#
#--------------------------------- parse tools -------------------------------------------------------------#
#----------------------------------------------------------------------------------------------------------#
        
class ParseTools(object):
    @staticmethod
    def str_to_boolean(val):
        if val:
            return val.lower().strip() in ['1', 'true']
        else:
            return False


#----------------------------------------------------------------------------------------------------------#
#--------------------------------- configuration tools ----------------------------------------------------#
#----------------------------------------------------------------------------------------------------------#
# This module contains tools for configuration

## JSON configuration class
# Dictionnary object saved in JSON format
class JSONConf(object):
    ## load configuration from file
    # @param file_path the full path of the configuration file
    # @return an object representing the configuration
    @staticmethod    
    def load_from_file(file_path):
        # a json file may content comment
        return json.loads(clean_comments(FileTools.read_text(file_path)))
    
    ## save configuration to file
    # @param conf the configuration object to save to disk
    # @param file_path the full path of the configuration file
    @staticmethod
    def save_to_file(conf, file_path):
        FileTools.save_text(json.dumps(conf, indent=4), file_path)
    
    ## parse the configuration file to get a simpler configuration file (i.e. simple key:value configuration)
    # @param dico_conf the configuration data (a complex dictionary)
    
    @staticmethod
    def parse_to_pretty(dico_conf, base_key=""):
        result = {}
        for key in dico_conf.keys():
            value = dico_conf[key]
            if isinstance(value, dict):
                dico = JSONConf.parse_to_pretty(value, base_key + key+ ".")
                result.update(dico)
            else:
                result[base_key + key] = value
        
        
        return result

## Exception raised in case of error in configuration file
class ConfigurationCheckerError(RuntimeError):
    pass
        
class ConfigurationChecker(object):
    ## check that some keys are present in the configuration
    # @param conf the configuration object to check
    # @param keys_list the list of keys that must be present in the configuration object
    # @return 
    @staticmethod
    def check_keys(conf, keys_list):
        if not conf:
            raise ConfigurationCheckerError('No conf provided !')
        test = True
        key_ko = []
        for key in keys_list:
            if key not in conf.keys():
                key_ko.append(key)
                test = False
        if not test:
            raise ConfigurationCheckerError('Param(s) %s not present in configuration!' % (str(key_ko)))
        

# Regular expression for comments
comment_re = re.compile(
    '(^)?[^\S\n]*/(?:\*(.*?)\*/[^\S\n]*|/[^\n]*)($)?',
    re.DOTALL | re.MULTILINE
)

def clean_comments(s_string):
#    a json file may accept comment but ignore it
#    see http://blog.getify.com/json-comments/
#    this code is inspired by damien riquet
#    http://www.lifl.fr/~riquetd/parse-a-json-file-with-comments.html
#    Comments look like :
#            // ...
#        or
#            /*
#            ...
#            */
    content = ''.join(s_string)
    ## Looking for comments
    match = comment_re.search(content)
    while match:
        # single line comment
        content = content[:match.start()] + content[match.end():]
        match = comment_re.search(content)
    
    return content


#----------------------------------------------------------------------------------------------------------#
#-------------------------------------- logging tools -----------------------------------------------------#
#----------------------------------------------------------------------------------------------------------#

# NullHandler class for lib coding purpose
class NullHandler(logging.Handler):
    def emit(self, record):
        pass
    
# A handler based on a queue. Records are just put on a queue
class QueueHandler(logging.Handler):
    def __init__(self, queue):
        logging.Handler.__init__(self)
        self._queue = queue
        
    def emit(self, record):
        self._queue.put_nowait(record)

# Queue log manager
class QueuedLogManager(object):
    @staticmethod
    def log_from_queue_for_ever(logger, queue):
        while True:
            try:
                if not queue.empty():
                    record = queue.get_nowait()
                    logger.callHandlers(record)
                else:
                    time.sleep(1)
            except:
                time.sleep(1)

class TestLogger(object):
    def __init__(self, queue):
        self._queue = queue
        
    def info(self, message):
        record = logging.makeLogRecord({"name": "test_queue_logger.local", "msg": message, "levelno": logging.INFO, "levelname": logging.getLevelName(logging.INFO)})
        self._queue.put_nowait(record)
